package cn.sciento.dd.service.impl;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import org.apache.commons.io.FileUtils;
import cn.sciento.dd.constant.DingConstans;
import cn.sciento.dd.constant.DingErrorCode;
import cn.sciento.dd.dto.CreateDeptResultDTO;
import cn.sciento.dd.dto.DefaultResultDTO;
import cn.sciento.dd.dto.Department;
import cn.sciento.dd.dto.DeptListDTO;
import cn.sciento.dd.dto.DingSyncDeptDTO;
import cn.sciento.dd.dto.DingSyncUserDTO;
import cn.sciento.dd.dto.GetUserDTO;
import cn.sciento.dd.dto.GetUserListDTO;
import cn.sciento.dd.dto.TokenDTO;
import cn.sciento.dd.dto.UpdateDeptResultDTO;
import cn.sciento.dd.dto.UserCreateResultDTO;
import cn.sciento.dd.service.DingCorpAddressService;
import cn.sciento.dd.service.DingTokenService;
import cn.sciento.dd.utils.LogUtils;
import cn.sciento.starter.integrate.dto.SyncCorpResultDTO;
import cn.sciento.starter.integrate.dto.SyncDeptDTO;
import cn.sciento.starter.integrate.dto.SyncUserDTO;
import cn.sciento.starter.integrate.entity.CorpHrSync;
import cn.sciento.starter.integrate.service.AbstractCorpSyncService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

@Service
public class DingCorpSyncServiceImpl implements AbstractCorpSyncService {
  private static final Logger logger = LoggerFactory.getLogger(DingCorpSyncServiceImpl.class);
  
  @Autowired
  private DingCorpAddressService dingCorpAddressService;
  
  @Autowired
  private DingTokenService dingTokenService;
  
  public String corpSyncType() {
    return "DD";
  }
  
  public String getAccessToken(CorpHrSync corpHrSync) {
    TokenDTO tokenDTO;
    TokenDTO result;
    switch (corpHrSync.getAuthType()) {
      case "SELF":
        tokenDTO = this.dingTokenService.getTokenWithCache(corpHrSync.getAppId(), corpHrSync.getAppSecret());
        if (StringUtils.hasText(tokenDTO.getAccess_token()))
          return tokenDTO.getAccess_token(); 
        return null;
      case "THIRD":
        result = this.dingTokenService.getTokenFromThirdPart(corpHrSync.getAuthAddress());
        if (result != null && StringUtils.hasText(result.getAccess_token()))
          return result.getAccess_token(); 
        return null;
    } 
    return null;
  }
  
  public SyncCorpResultDTO syncCorp(List<SyncDeptDTO> syncDeptList, List<SyncUserDTO> syncUserList, String fileName, Boolean useGeneratedDeptId, String accessToken) {
    SyncCorpResultDTO syncCorpResultDTO = (new SyncCorpResultDTO()).setDeptStatus(Boolean.valueOf(true)).setEmployeeStatus(Boolean.valueOf(true));
    File file = new File(FileUtils.getTempDirectoryPath() + File.separator + "syncCorp" + File.separator + fileName);
    Map<Long, Long> successDeptIdMap = new HashMap<>(16);
    List<String> successUserIds = new ArrayList<>();
    try {
      List<String> deleteUserIds = new ArrayList<>();
      List<DingSyncUserDTO> insertUsers = new ArrayList<>();
      Map<String, DingSyncUserDTO> updateUsersMap = new HashMap<>(16);
      List<Long> deleteDeptIds = new ArrayList<>();
      List<DingSyncDeptDTO> insertDeptList = new ArrayList<>();
      List<DingSyncDeptDTO> updateDeptList = new ArrayList<>();
      List<DingSyncUserDTO> dingSyncUsers = new ArrayList<>();
      List<DingSyncDeptDTO> weChatSyncDeptDTOList = new ArrayList<>();
      convertSyncUserList(syncUserList, dingSyncUsers);
      convertSyncDeptList(syncDeptList, weChatSyncDeptDTOList);
      dingSyncUsers.forEach(dingSyncUserDTO -> {
            switch (dingSyncUserDTO.getSyncType()) {
              case "create":
                insertUsers.add(dingSyncUserDTO);
                return;
              case "update":
                updateUsersMap.put(dingSyncUserDTO.getUserid(), dingSyncUserDTO);
                return;
              case "delete":
                deleteUserIds.add(dingSyncUserDTO.getUserid());
                return;
            } 
            LogUtils.info(logger, file, "Employees synchronization object has no synchronization type: " + dingSyncUserDTO.toString());
          });
      weChatSyncDeptDTOList.forEach(dingSyncDeptDTO -> {
            switch (dingSyncDeptDTO.getSyncType()) {
              case "create":
                insertDeptList.add(dingSyncDeptDTO);
                return;
              case "update":
                updateDeptList.add(dingSyncDeptDTO);
                return;
              case "delete":
                deleteDeptIds.add(dingSyncDeptDTO.getId());
                return;
            } 
            LogUtils.info(logger, file, "Departments synchronization object has no synchronization type: " + dingSyncDeptDTO.toString());
          });
      ResultCount resultCount = new ResultCount();
      LogUtils.info(logger, file, "***Start syncing DingTalk department***");
      insertDeptList(file, insertDeptList, successDeptIdMap, useGeneratedDeptId, accessToken, resultCount);
      deleteDeptList(file, deleteDeptIds, deleteUserIds, updateUsersMap, successDeptIdMap, successUserIds, accessToken, resultCount);
      updateDeptList(file, updateDeptList, successDeptIdMap, accessToken, resultCount);
      LogUtils.info(logger, file, "**Start syncing DingTalk employees**");
      deleteUsers(file, deleteUserIds, successUserIds, accessToken, resultCount);
      insertUsers(file, insertUsers, successDeptIdMap, successUserIds, accessToken, resultCount);
      updateUsers(file, updateUsersMap, successDeptIdMap, successUserIds, accessToken, resultCount);
      if (resultCount.getDeptFailCount() > 0)
        syncCorpResultDTO.setDeptStatus(Boolean.valueOf(false)); 
      if (resultCount.getUserFailCount() > 0)
        syncCorpResultDTO.setEmployeeStatus(Boolean.valueOf(false)); 
      LogUtils.info(logger, file, "Sync is complete. Departments Synced successfully count: " + resultCount
          
          .getDeptSuccessCount() + "，synced failed count: " + resultCount
          .getDeptFailCount() + ".Employees synced successfully count: " + resultCount
          .getUserSuccessCount() + ",synced failed count: " + resultCount
          .getUserFailCount() + ".");
      return getSyncCorpResultDTO(syncCorpResultDTO, file).setDeptIdMap(successDeptIdMap)
        .setUserIds(successUserIds);
    } catch (Exception e) {
      LogUtils.error(logger, file, e.toString());
      return getSyncCorpResultDTO(syncCorpResultDTO.setEmployeeStatus(Boolean.valueOf(false)).setDeptStatus(Boolean.valueOf(false)), file)
        .setDeptIdMap(successDeptIdMap).setUserIds(successUserIds);
    } 
  }
  
  private void convertSyncUserList(List<SyncUserDTO> syncUserList, List<DingSyncUserDTO> dingSyncUserList) {
    syncUserList.forEach(e -> {
          DingSyncUserDTO dingSyncUserDTO = new DingSyncUserDTO();
          BeanUtils.copyProperties(e, dingSyncUserDTO);
          dingSyncUserList.add(dingSyncUserDTO);
        });
  }
  
  private void convertSyncDeptList(List<SyncDeptDTO> syncDeptList, List<DingSyncDeptDTO> dingSyncDeptList) {
    syncDeptList.forEach(e -> {
          DingSyncDeptDTO dingSyncDeptDTO = new DingSyncDeptDTO();
          BeanUtils.copyProperties(e, dingSyncDeptDTO);
          dingSyncDeptList.add(dingSyncDeptDTO);
        });
  }
  
  private void insertDeptList(File file, List<DingSyncDeptDTO> insertDeptList, Map<Long, Long> successDeptIdMap, Boolean useGeneratedDeptId, String accessToken, ResultCount resultCount) {
    if (!CollectionUtils.isEmpty(insertDeptList)) {
      ArrayList<DingSyncDeptDTO> dingSyncDeptDTOList = sortDept(insertDeptList);
      for (DingSyncDeptDTO j : dingSyncDeptDTOList) {
        Long deptId = j.getId();
        if (useGeneratedDeptId.booleanValue())
          j.setId(null); 
        boolean isParentIdMap = convertDeptParentId(j, successDeptIdMap);
        LogUtils.info(logger, file, "Create DingTalk department:" + j.toString());
        if (!isParentIdMap) {
          LogUtils.error(logger, file, "Create failed,the parent department does not exist.");
          resultCount.deptFail();
          continue;
        } 
        CreateDeptResultDTO dept = this.dingCorpAddressService.createDept(accessToken, j);
        if (DingErrorCode.SUCCESS.equals(Integer.valueOf(dept.getErrcode()))) {
          successDeptIdMap.put(deptId, dept.getId());
          resultCount.deptSuccess();
          continue;
        } 
        if (DingErrorCode.DEPT_EXISTS.equals(Integer.valueOf(dept.getErrcode())) && !useGeneratedDeptId.booleanValue()) {
          LogUtils.info(logger, file, "Department already exists,update department");
          UpdateDeptResultDTO updateDeptResultDTO = this.dingCorpAddressService.updateDept(accessToken, j);
          if (DingErrorCode.SUCCESS.equals(Integer.valueOf(updateDeptResultDTO.getErrcode()))) {
            successDeptIdMap.put(deptId, deptId);
            resultCount.deptSuccess();
            continue;
          } 
          resultCount.deptFail();
          LogUtils.error(logger, file, "Update failed: " + updateDeptResultDTO.getErrmsg());
          continue;
        } 
        resultCount.deptFail();
        LogUtils.error(logger, file, "Create failed: " + dept.getErrmsg());
      } 
    } 
  }
  
  private void deleteDeptList(File file, List<Long> deleteDeptIds, List<String> deleteUserIds, Map<String, DingSyncUserDTO> updateUsersMap, Map<Long, Long> successDeptIdMap, List<String> successUserIds, String accessToken, ResultCount resultCount) {
    while (!CollectionUtils.isEmpty(deleteDeptIds)) {
      Long deptId = deleteDeptIds.get(0);
      DeptListDTO deptList = this.dingCorpAddressService.getDeptList(deptId, accessToken, "", "true");
      Department rootDept = new Department();
      rootDept.setId(deptId.longValue());
      deptList.getDepartment().add(rootDept);
      if (!CollectionUtils.isEmpty(deptList.getDepartment())) {
        LogUtils.info(logger, file, "Delete the DingTalk department and its sub-departments id:" + deptId);
        deleteDeptIds.removeAll((Collection)deptList
            .getDepartment().stream().map(Department::getId).collect(Collectors.toList()));
        ArrayList<Department> departments = sortDept(deptList.getDepartment(), deptId);
        for (Department k : departments) {
          LogUtils.info(logger, file, "Delete DingTalk department，id:" + k.getId());
          LogUtils.info(logger, file, "Change users under the department before deleting the DingTalk department");
          GetUserListDTO userListByDeptId = this.dingCorpAddressService.getUserListByDeptId(accessToken, k.getId() + "");
          List<String> userIds = userListByDeptId.getUserIds();
          if (!CollectionUtils.isEmpty(userIds))
            for (String userId : userIds) {
              if (deleteUserIds.contains(userId)) {
                deleteUserIds.remove(userId);
                LogUtils.info(logger, file, "Delete employees under department: userid" + userId);
                DefaultResultDTO defaultResultDTO1 = this.dingCorpAddressService.deleteUserById(accessToken, userId);
                if (!DingErrorCode.SUCCESS.equals(Integer.valueOf(defaultResultDTO1.getErrcode()))) {
                  resultCount.userFail();
                  LogUtils.error(logger, file, "Delete failed：" + defaultResultDTO1.getErrmsg());
                  continue;
                } 
                successUserIds.add(userId);
                resultCount.userSuccess();
                continue;
              } 
              if (updateUsersMap.containsKey(userId)) {
                DingSyncUserDTO dingSyncUserDTO = updateUsersMap.get(userId);
                updateUsersMap.remove(userId);
                boolean isAllDepartIdsMap = convertUserDepart(dingSyncUserDTO, successDeptIdMap);
                LogUtils.info(logger, file, "Update department DingTalk employees:" + dingSyncUserDTO.toString());
                if (!isAllDepartIdsMap) {
                  LogUtils.error(logger, file, "Update employee failed, department does not exist");
                  resultCount.userFail();
                  continue;
                } 
                DefaultResultDTO defaultResultDTO1 = this.dingCorpAddressService.updateUser(accessToken, dingSyncUserDTO);
                if (!DingErrorCode.SUCCESS.equals(Integer.valueOf(defaultResultDTO1.getErrcode()))) {
                  LogUtils.error(logger, file, "Update failed: " + dingSyncUserDTO.toString());
                  resultCount.userFail();
                  continue;
                } 
                successUserIds.add(userId);
                resultCount.userSuccess();
                continue;
              } 
              GetUserDTO userInfoByUserId = this.dingCorpAddressService.getUserInfoByUserId(accessToken, userId + "", "");
              LogUtils.error(logger, file, "The employee is not included in the sync employees: " + userInfoByUserId
                  .toString());
            }  
          DefaultResultDTO defaultResultDTO = this.dingCorpAddressService.deleteDeptById(accessToken, k.getId() + "");
          if (DingErrorCode.SUCCESS.equals(Integer.valueOf(defaultResultDTO.getErrcode()))) {
            successDeptIdMap.put(Long.valueOf(k.getId()), null);
            resultCount.deptSuccess();
            continue;
          } 
          LogUtils.error(logger, file, "Delete department failed: " + defaultResultDTO.getErrmsg());
          resultCount.deptFail();
        } 
      } 
    } 
  }
  
  private void updateDeptList(File file, List<DingSyncDeptDTO> updateDeptList, Map<Long, Long> successDeptIdMap, String accessToken, ResultCount resultCount) {
    if (!CollectionUtils.isEmpty(updateDeptList))
      for (DingSyncDeptDTO j : updateDeptList) {
        boolean isParentIdMap = convertDeptParentId(j, successDeptIdMap);
        LogUtils.info(logger, file, "Update DingTalk department:" + j.toString());
        if (!isParentIdMap) {
          resultCount.deptFail();
          LogUtils.error(logger, file, "Update failed, parent department does not exist.");
          continue;
        } 
        UpdateDeptResultDTO updateDeptResultDTO = this.dingCorpAddressService.updateDept(accessToken, j);
        if (DingErrorCode.SUCCESS.equals(Integer.valueOf(updateDeptResultDTO.getErrcode()))) {
          successDeptIdMap.put(j.getId(), null);
          resultCount.deptSuccess();
          continue;
        } 
        LogUtils.error(logger, file, "Update failed: " + updateDeptResultDTO.getErrmsg());
        resultCount.deptFail();
      }  
  }
  
  private void deleteUsers(File file, List<String> deleteUserIds, List<String> successUserIds, String accessToken, ResultCount resultCount) {
    if (!CollectionUtils.isEmpty(deleteUserIds))
      for (String j : deleteUserIds) {
        LogUtils.info(logger, file, "Delete DingTalk employees: userid" + j);
        DefaultResultDTO defaultResultDTO = this.dingCorpAddressService.deleteUserById(accessToken, j);
        if (!DingErrorCode.SUCCESS.equals(Integer.valueOf(defaultResultDTO.getErrcode()))) {
          LogUtils.error(logger, file, "Delete failed：" + defaultResultDTO.getErrmsg());
          resultCount.userFail();
          continue;
        } 
        successUserIds.add(j);
        resultCount.userSuccess();
      }  
  }
  
  private void insertUsers(File file, List<DingSyncUserDTO> insertUsers, Map<Long, Long> successDeptIdMap, List<String> successUserIds, String accessToken, ResultCount resultCount) {
    if (!CollectionUtils.isEmpty(insertUsers))
      for (DingSyncUserDTO j : insertUsers) {
        boolean isAllDepartIdsMap = convertUserDepart(j, successDeptIdMap);
        LogUtils.info(logger, file, "Create DingTalk employees:" + j.toString());
        if (!isAllDepartIdsMap) {
          LogUtils.error(logger, file, "Create employees failed, department does not exist.");
          resultCount.userFail();
          continue;
        } 
        UserCreateResultDTO user = this.dingCorpAddressService.createUser(accessToken, j);
        if (DingErrorCode.SUCCESS.equals(Integer.valueOf(user.getErrcode()))) {
          successUserIds.add(j.getUserid());
          resultCount.userSuccess();
          continue;
        } 
        if (DingErrorCode.USERID_EXISTS.equals(Integer.valueOf(user.getErrcode()))) {
          LogUtils.info(logger, file, "Employee already exists, update employee");
          DefaultResultDTO defaultResultDTO = this.dingCorpAddressService.updateUser(accessToken, j);
          if (DingErrorCode.SUCCESS.equals(Integer.valueOf(defaultResultDTO.getErrcode()))) {
            successUserIds.add(j.getUserid());
            resultCount.userSuccess();
            continue;
          } 
          resultCount.userFail();
          LogUtils.error(logger, file, "Update failed: " + defaultResultDTO.getErrmsg());
          continue;
        } 
        resultCount.userFail();
        LogUtils.error(logger, file, "Create failed: " + user.getErrmsg());
      }  
  }
  
  private void updateUsers(File file, Map<String, DingSyncUserDTO> updateUsersMap, Map<Long, Long> successDeptIdMap, List<String> successUserIds, String accessToken, ResultCount resultCount) {
    if (!CollectionUtils.isEmpty(updateUsersMap.values()))
      for (DingSyncUserDTO j : updateUsersMap.values()) {
        boolean isAllDepartIdsMap = convertUserDepart(j, successDeptIdMap);
        LogUtils.info(logger, file, "Update DingTalk employees: " + j.toString());
        if (!isAllDepartIdsMap) {
          LogUtils.error(logger, file, "Update employee failed, department does not exist.");
          resultCount.userFail();
          continue;
        } 
        DefaultResultDTO defaultResultDTO = this.dingCorpAddressService.updateUser(accessToken, j);
        if (DingErrorCode.SUCCESS.equals(Integer.valueOf(defaultResultDTO.getErrcode()))) {
          successUserIds.add(j.getUserid());
          resultCount.userSuccess();
          continue;
        } 
        LogUtils.error(logger, file, "Update failed: " + defaultResultDTO.getErrmsg());
        resultCount.userFail();
      }  
  }
  
  public static class ResultCount {
    private int deptSuccessCount = 0;
    
    private int deptFailCount = 0;
    
    private int userSuccessCount = 0;
    
    private int userFailCount = 0;
    
    void deptSuccess() {
      this.deptSuccessCount++;
    }
    
    void deptFail() {
      this.deptFailCount++;
    }
    
    void userSuccess() {
      this.userSuccessCount++;
    }
    
    void userFail() {
      this.userFailCount++;
    }
    
    int getDeptSuccessCount() {
      return this.deptSuccessCount;
    }
    
    int getDeptFailCount() {
      return this.deptFailCount;
    }
    
    int getUserSuccessCount() {
      return this.userSuccessCount;
    }
    
    int getUserFailCount() {
      return this.userFailCount;
    }
  }
  
  protected SyncCorpResultDTO getSyncCorpResultDTO(SyncCorpResultDTO syncCorpResultDTO, File file) {
    try {
      syncCorpResultDTO.setLog(FileUtils.readFileToString(file, StandardCharsets.UTF_8));
    } catch (IOException e) {
      logger.error("Failed to read log from file {0}", e);
    } 
    FileUtils.deleteQuietly(file);
    return syncCorpResultDTO;
  }
  
  private ArrayList<DingSyncDeptDTO> sortDept(List<DingSyncDeptDTO> syncDeptList) {
    ArrayList<DingSyncDeptDTO> sortDept = new ArrayList<>();
    List<Long> collect = (List<Long>)syncDeptList.stream().map(DingSyncDeptDTO::getId).collect(Collectors.toList());
    boolean init = true;
    while (!CollectionUtils.isEmpty(syncDeptList)) {
      if (init) {
        List<Long> finalCollect = collect;
        List<DingSyncDeptDTO> rootDept = (List<DingSyncDeptDTO>)syncDeptList.stream().filter(e -> (DingConstans.ROOT_DEPT_ID.equals(e.getParentid()) || !finalCollect.contains(e.getParentUnitId()))).collect(Collectors.toList());
        collect = (List<Long>)rootDept.stream().map(DingSyncDeptDTO::getId).collect(Collectors.toList());
        sortDept.addAll(rootDept);
        syncDeptList.removeAll(rootDept);
        init = false;
        continue;
      } 
      List<Long> finalCollect = collect;
      List<DingSyncDeptDTO> deptList = (List<DingSyncDeptDTO>)syncDeptList.stream().filter(e -> finalCollect.contains(e.getParentUnitId())).collect(Collectors.toList());
      collect = (List<Long>)deptList.stream().map(DingSyncDeptDTO::getId).collect(Collectors.toList());
      sortDept.addAll(deptList);
      syncDeptList.removeAll(deptList);
    } 
    return sortDept;
  }
  
  private ArrayList<Department> sortDept(List<Department> departmentBeans, Long rootId) {
    List<Long> rootIds = null;
    Long finalRootId = rootId;
    boolean init = true;
    ArrayList<Department> sortDept = new ArrayList<>();
    while (!CollectionUtils.isEmpty(departmentBeans)) {
      if (init) {
        List<Department> rootDept = (List<Department>)departmentBeans.stream().filter(j -> (j.getId() == finalRootId.longValue())).collect(Collectors.toList());
        sortDept.addAll(rootDept);
        departmentBeans.removeAll(rootDept);
        rootDept = (List<Department>)departmentBeans.stream().filter(j -> Objects.equals(j.getParentid(), finalRootId)).collect(Collectors.toList());
        sortDept.addAll(rootDept);
        departmentBeans.removeAll(rootDept);
        rootIds = (List<Long>)rootDept.stream().map(Department::getId).collect(Collectors.toList());
        init = false;
        continue;
      } 
      ArrayList<Department> otherDept = new ArrayList<>();
      for (Department departmentBean : departmentBeans) {
        if (rootIds.contains(departmentBean.getParentid()))
          otherDept.add(departmentBean); 
      } 
      sortDept.addAll(otherDept);
      departmentBeans.removeAll(otherDept);
      rootIds = (List<Long>)otherDept.stream().map(Department::getId).collect(Collectors.toList());
    } 
    Collections.reverse(sortDept);
    return sortDept;
  }
  
  private boolean convertDeptParentId(DingSyncDeptDTO syncDeptDTO, Map<Long, Long> successDeptIdMap) {
    if (!DingConstans.ROOT_DEPT_ID.equals(syncDeptDTO.getParentid()) && syncDeptDTO.getParentid() == null) {
      if (successDeptIdMap.containsKey(syncDeptDTO.getParentUnitId())) {
        syncDeptDTO.setParentid(successDeptIdMap.get(syncDeptDTO.getParentUnitId()));
        return true;
      } 
      return false;
    } 
    return true;
  }
  
  private boolean convertUserDepart(DingSyncUserDTO syncUserDTO, Map<Long, Long> successDeptIdMap) {
    List<Long> department = syncUserDTO.getDepartment();
    List<Boolean> isDepartIdsMap = syncUserDTO.getIsDepartIdsMap();
    boolean isAllDepartIdsMap = true;
    for (int i = 0; i < isDepartIdsMap.size(); i++) {
      if (isDepartIdsMap.get(i) != null && !((Boolean)isDepartIdsMap.get(i)).booleanValue()) {
        Long departId = successDeptIdMap.get(department.get(i));
        if (departId != null) {
          department.set(i, departId);
        } else {
          isAllDepartIdsMap = false;
        } 
      } 
    } 
    return isAllDepartIdsMap;
  }
}
